using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.ComponentModel;
using System.Globalization;
using C4F.DevKit.WebServices.Properties;
using C4F.DevKit.WebServices.Datasets;

namespace C4F.DevKit.WebServices
{
    /// <summary>
    /// This class is a wrapper over Yahoo traffic service.
    /// </summary>
    [ToolboxItem(true)]
    public class YahooTraffic : C4FWebServicesBase
    {
        #region Private members
        /// <summary>
        /// Base Url
        /// </summary>
        private static string baseUrl = Resources.YahooTrafficBaseUrl;
        
        /// <summary>
        /// Application Key required to use the Api
        /// </summary>
        private string applicationKey = string.Empty;

        #endregion

        #region Accessors

        /// <summary>
        /// Gets/sets application key for yahoo traffic service.
        /// </summary>
        [Category("YahooTraffic")]
        [Description("Gets/sets application key for yahoo traffic service.")]
        public string ApplicationKey
        {
            get { return this.applicationKey; }
            set { this.applicationKey = value == null ? string.Empty : value.Trim(); }
        }

        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new object of YahooTraffic class
        /// </summary>
        public YahooTraffic() { }
        /// <summary>
        /// Initializes a new object of YahooTraffic class with an application key
        /// </summary>
        /// <param name="appKey">Application key</param>
        public YahooTraffic(string appKey)
        {
            this.ApplicationKey = appKey;
        }

        #endregion

        #region Public functions
        /// <summary>
        /// Get traffic alert information from a given location
        /// </summary>
        /// <param name="street">Name of the street</param>
        /// <param name="city">Name of the city</param>
        /// <param name="state">The United States state</param>
        /// <param name="zip">Zip code of the location</param>
        /// <param name="location">If location is specified, it will take priority over the individual fields in determining the location for the query</param>
        /// <param name="latitude">he latitude of the starting location</param>
        /// <param name="longitude">The longitude of the starting location. If both latitude and longitude are specified, they will take priority over all other location data</param>
        /// <param name="severity">The minimum severity level to return, from 1 (least severe) to 5 (most severe)</param>
        /// <param name="zoom">The zoom level for the data, from 1 (street level) to 12 (country level)</param>
        /// <param name="radius">How far (in miles) from the specified location to provide data for</param>
        /// <param name="include_map">If set to 1 will return the URL of a map image for the location of each reported incident</param>
        /// <param name="image_type">The image format for the map</param>
        /// <param name="image_height">The height of the image</param>
        /// <param name="image_width">The width of the image</param>
        /// <returns>The traffic incidents matching the criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set</exception>
        public List<TrafficIncident> GetTraffic(string street, string city,
            State state, string zip, string location, float latitude,
            float longitude, int? severity, int? zoom, int radius, int? include_map,
            string image_type, int? image_height, int? image_width)
        {
            if (String.IsNullOrEmpty(this.ApplicationKey))
                throw new InvalidOperationException(Resources.ErrorApplicationKeyNotSet);

            string requestUrl = this.GetQueryString(street, city, state, zip,
                location, latitude, longitude, severity, zoom, radius,
                include_map, image_type, image_height, image_width);

            // Get traffic responses
            string responseData = base.GetQueryResponse(requestUrl);

            // Parse the data and return results
            return YahooTraffic.ParseResponseXml(responseData);
        }

        /// <summary>
        /// Get traffic alert information from a given location
        /// </summary>
        /// <param name="street">Street name</param>
        /// <param name="city">City name</param>
        /// <param name="state">The United States state</param>
        /// <returns>The traffic incidents matching the criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set</exception>
        public List<TrafficIncident> GetTraffic(string street, string city, State state)
        {
            if (String.IsNullOrEmpty(this.ApplicationKey))
                throw new InvalidOperationException(Resources.ErrorApplicationKeyNotSet);

            string requestUrl = this.GetQueryString(street, city, state, string.Empty, string.Empty, 0, 0, null, null, 0, null, null, null, null);

            // Get traffic responses
            string responseData = base.GetQueryResponse(requestUrl);

            // Parse the data and return results
            return YahooTraffic.ParseResponseXml(responseData);
        }

        /// <summary>
        /// Get traffic alert information from a given location
        /// </summary>
        /// <param name="location">The location can be 'city, state', 'city, state, zip', 'zip', 'street, city, state', 'street, city, state, zip' or 'street, zip'</param>
        /// <returns>The traffic incidents matching the criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set</exception>
        public List<TrafficIncident> GetTraffic(string location)
        {
            if (String.IsNullOrEmpty(this.ApplicationKey))
                throw new InvalidOperationException(Resources.ErrorApplicationKeyNotSet);

            string requestUrl = this.GetQueryString(string.Empty, string.Empty, State._AllStates, string.Empty, location, 0, 0, null, null, 0, null, null, null, null);

            // Get traffic responses
            string responseData = base.GetQueryResponse(requestUrl);

            // Parse the data and return results
            return YahooTraffic.ParseResponseXml(responseData);
        }

        /// <summary>
        /// Get traffic alert information from a given location
        /// </summary>
        /// <param name="location">The location can be 'city, state', 'city, state, zip', 'zip', 'street, city, state', 'street, city, state, zip' or 'street, zip'</param>
        /// <returns>Dataset containing the traffic incidents matching the criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set</exception>
        public DS_YahooTraffic GetTraffic_DS(string location)
        {
            List<TrafficIncident> incidents = GetTraffic(location);
            DS_YahooTraffic dataset = new DS_YahooTraffic();

            foreach (TrafficIncident incident in incidents)
            {
                DS_YahooTraffic.LocationRow locationRow = dataset.Location.AddLocationRow(incident.IncidentDetails.Location.Latitude,
                    incident.IncidentDetails.Location.Longitude);
                DS_YahooTraffic.IncidentDetailsRow incidentDetailsRow = dataset.IncidentDetails.AddIncidentDetailsRow(incident.IncidentDetails.Direction,
                    incident.IncidentDetails.Severity, incident.IncidentDetails.ReportDate, incident.IncidentDetails.UpdateDate, incident.IncidentDetails.EndDate);
                dataset.TrafficIncident.AddTrafficIncidentRow(incident.Type, incident.Description, incident.ImageUrl, locationRow, incidentDetailsRow);
            }
            return dataset;
        }

        /// <summary>
        /// Get traffic alert information from a given location
        /// </summary>
        /// <param name="street">Street name</param>
        /// <param name="city">City name</param>
        /// <param name="state">The United States state</param>
        /// <returns>Dataset containing the traffic incidents matching the criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set</exception>
        public DS_YahooTraffic GetTraffic_DS(string street, string city, State state)
        {
            List<TrafficIncident> incidents = GetTraffic(street, city, state);
            DS_YahooTraffic dataset = new DS_YahooTraffic();

            foreach (TrafficIncident incident in incidents)
            {
                DS_YahooTraffic.LocationRow locationRow = dataset.Location.AddLocationRow(incident.IncidentDetails.Location.Latitude,
                    incident.IncidentDetails.Location.Longitude);
                DS_YahooTraffic.IncidentDetailsRow incidentDetailsRow = dataset.IncidentDetails.AddIncidentDetailsRow(incident.IncidentDetails.Direction,
                    incident.IncidentDetails.Severity, incident.IncidentDetails.ReportDate, incident.IncidentDetails.UpdateDate, incident.IncidentDetails.EndDate);
                dataset.TrafficIncident.AddTrafficIncidentRow(incident.Type, incident.Description, incident.ImageUrl, locationRow, incidentDetailsRow);
            }
            return dataset;
        }

        /// <summary>
        /// Get traffic alert information from a given location
        /// </summary>
        /// <param name="street">Name of the street</param>
        /// <param name="city">Name of the city</param>
        /// <param name="state">The United States state</param>
        /// <param name="zip">Zip code of the location</param>
        /// <param name="location">If location is specified, it will take priority over the individual fields in determining the location for the query</param>
        /// <param name="latitude">he latitude of the starting location</param>
        /// <param name="longitude">The longitude of the starting location. If both latitude and longitude are specified, they will take priority over all other location data</param>
        /// <param name="severity">The minimum severity level to return, from 1 (least severe) to 5 (most severe)</param>
        /// <param name="zoom">The zoom level for the data, from 1 (street level) to 12 (country level)</param>
        /// <param name="radius">How far (in miles) from the specified location to provide data for</param>
        /// <param name="include_map">If set to 1 will return the URL of a map image for the location of each reported incident</param>
        /// <param name="image_type">The image format for the map</param>
        /// <param name="image_height">The height of the image</param>
        /// <param name="image_width">The width of the image</param>
        /// <returns>Dataset containing the traffic incidents matching the criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set</exception>
        public DS_YahooTraffic GetTraffic_DS(string street, string city,
            State state, string zip, string location, float latitude,
            float longitude, int? severity, int? zoom, int radius, int? include_map,
            string image_type, int? image_height, int? image_width)
        {
            List<TrafficIncident> incidents = GetTraffic(street, city, state, zip, location, latitude, longitude, severity,
                zoom, radius, include_map, image_type, image_height, image_width);
            DS_YahooTraffic dataset = new DS_YahooTraffic();

            foreach (TrafficIncident incident in incidents)
            {
                DS_YahooTraffic.LocationRow locationRow = dataset.Location.AddLocationRow(incident.IncidentDetails.Location.Latitude,
                    incident.IncidentDetails.Location.Longitude);
                DS_YahooTraffic.IncidentDetailsRow incidentDetailsRow = dataset.IncidentDetails.AddIncidentDetailsRow(incident.IncidentDetails.Direction,
                    incident.IncidentDetails.Severity, incident.IncidentDetails.ReportDate, incident.IncidentDetails.UpdateDate, incident.IncidentDetails.EndDate);
                dataset.TrafficIncident.AddTrafficIncidentRow(incident.Type, incident.Description, incident.ImageUrl, locationRow, incidentDetailsRow);
            }
            return dataset;
        }

        #endregion

        #region Private functions

        /// <summary>
        /// Get request Url
        /// </summary>
        /// <param name="street">Street name</param>
        /// <param name="city">City name</param>
        /// <param name="state">The United States state</param>
        /// <param name="zip">Zip code</param>
        /// <param name="location">If location is specified, it will take priority over the individual fields in determining the location for the query</param>
        /// <param name="latitude">he latitude of the starting location</param>
        /// <param name="longitude">The longitude of the starting location. If both latitude and longitude are specified, they will take priority over all other location data</param>
        /// <param name="severity">The minimum severity level to return, from 1 (least severe) to 5 (most severe)</param>
        /// <param name="zoom">The zoom level for the data, from 1 (street level) to 12 (country level)</param>
        /// <param name="radius">How far (in miles) from the specified location to provide data for</param>
        /// <param name="include_map">If set to 1 will return the URL of a map image for the location of each reported incident</param>
        /// <param name="image_type">The image format for the map</param>
        /// <param name="image_height">The height of the image</param>
        /// <param name="image_width">The width of the image</param>
        /// <returns>Request Url</returns>
        private string GetQueryString(string street, string city, State state,
            string zip, string location, float latitude, float longitude,
            int? severity, int? zoom, int radius, int? include_map, string image_type,
            int? image_height, int? image_width)
        {
            //TODO: Add business validations here

            StringBuilder queryBuilder = new StringBuilder(YahooTraffic.baseUrl);
            // Add application key
            queryBuilder.Append("?appid=");
            queryBuilder.Append(this.ApplicationKey);
            // Add street
            queryBuilder.Append("&street=");
            queryBuilder.Append(street);
            // Add city
            queryBuilder.Append("&city=");
            queryBuilder.Append(city);
            // Add state
            queryBuilder.Append("&state=");
            queryBuilder.Append(Enum.GetName(typeof(StateAbbreviation), state));
            // Add zip
            queryBuilder.Append("&zip=");
            queryBuilder.Append(zip);
            // Add location
            queryBuilder.Append("&location=");
            queryBuilder.Append(location);

            if (latitude != 0 && longitude != 0)
            {
                // Add latitude
                queryBuilder.Append("&latitude=");
                queryBuilder.Append(latitude.ToString(CultureInfo.InvariantCulture));
                // Add longitude
                queryBuilder.Append("&longitude=");
                queryBuilder.Append(longitude.ToString(CultureInfo.InvariantCulture));
            }
            // Add severity
            if (severity.HasValue)
            {
                queryBuilder.Append("&severity=");
                queryBuilder.Append(severity.Value.ToString(CultureInfo.InvariantCulture));
            }
            // Add zoom
            if (zoom.HasValue)
            {
                queryBuilder.Append("&zoom=");
                queryBuilder.Append(zoom.Value.ToString(CultureInfo.InvariantCulture));
            }
            // Add radius
            if (radius != 0)
            {
                queryBuilder.Append("&radius=");
                queryBuilder.Append(radius.ToString(CultureInfo.InvariantCulture));
            }
            // Add include_map
            if (include_map.HasValue)
            {
                queryBuilder.Append("&include_map=");
                queryBuilder.Append(include_map.ToString());
            }
            // Add image_type
            if (image_type != null)
            {
                queryBuilder.Append("&image_type=");
                queryBuilder.Append(image_type.ToString());
            }
            // Add image_height
            if (image_height.HasValue)
            {
                queryBuilder.Append("&image_height=");
                queryBuilder.Append(image_height.ToString());
            }
            // Add image_width
            if (image_width.HasValue)
            {
                queryBuilder.Append("&image_width=");
                queryBuilder.Append(image_width.ToString());
            }

            return queryBuilder.ToString();
        }

        /// <summary>
        /// Parses the response data and extracts the traffic incidents
        /// </summary>
        /// <param name="responseData">Response data</param>
        /// <returns>A list of traffic incidents</returns>
        private static List<TrafficIncident> ParseResponseXml(string responseData)
        {
            XmlDocument xmlDocument = new XmlDocument();
            List<TrafficIncident> trafficIncidents = new List<TrafficIncident>();
            XmlNamespaceManager nsmanager = new XmlNamespaceManager(xmlDocument.NameTable);
            nsmanager.AddNamespace("maps", "urn:yahoo:maps");

            xmlDocument.LoadXml(responseData);
            // Retrieve result nodes
            XmlNodeList trafficIncidentNodes = xmlDocument.DocumentElement.SelectNodes("maps:Result", nsmanager);

            if (trafficIncidentNodes.Count == 0)
                return trafficIncidents;

            // Set a base date to enable conversion of Unix timestamps returned in xml to DateTime object
            System.DateTime baseDateTime = new System.DateTime(1970, 1, 1, 0, 0, 0, 0);

            TrafficIncident trafficIncident = null;
            foreach (XmlNode trafficIncidentNode in trafficIncidentNodes)
            {
                trafficIncident = new TrafficIncident();
                trafficIncident.IncidentDetails = new IncidentDetails();
                trafficIncident.IncidentDetails.Location = new Location();
                // Populate traffic incident object
                trafficIncident.Type = trafficIncidentNode.Attributes["type"].Value;
                trafficIncident.Description = trafficIncidentNode.SelectSingleNode("maps:Description", nsmanager).InnerText;
                trafficIncident.IncidentDetails.Location.Latitude = float.Parse(trafficIncidentNode.SelectSingleNode("maps:Latitude", nsmanager).InnerText, CultureInfo.InvariantCulture);
                trafficIncident.IncidentDetails.Location.Longitude = float.Parse(trafficIncidentNode.SelectSingleNode("maps:Longitude", nsmanager).InnerText, CultureInfo.InvariantCulture);
                trafficIncident.IncidentDetails.Direction = trafficIncidentNode.SelectSingleNode("maps:Direction", nsmanager).InnerText;
                trafficIncident.IncidentDetails.Severity = Convert.ToInt32(trafficIncidentNode.SelectSingleNode("maps:Severity", nsmanager).InnerText, CultureInfo.InvariantCulture);
                trafficIncident.IncidentDetails.ReportDate = baseDateTime.AddSeconds(Convert.ToDouble(trafficIncidentNode.SelectSingleNode("maps:ReportDate", nsmanager).InnerText, CultureInfo.InvariantCulture));
                trafficIncident.IncidentDetails.UpdateDate = baseDateTime.AddSeconds(Convert.ToDouble(trafficIncidentNode.SelectSingleNode("maps:UpdateDate", nsmanager).InnerText, CultureInfo.InvariantCulture));
                trafficIncident.IncidentDetails.EndDate = baseDateTime.AddSeconds(Convert.ToDouble(trafficIncidentNode.SelectSingleNode("maps:EndDate", nsmanager).InnerText, CultureInfo.InvariantCulture));

                XmlNode imageUrlNode = trafficIncidentNode.SelectSingleNode("maps:ImageUrl", nsmanager);

                if (imageUrlNode != null)
                    trafficIncident.ImageUrl = imageUrlNode.InnerText;

                trafficIncidents.Add(trafficIncident);
            }
            return trafficIncidents;
        }
        #endregion
    }

    /// <summary>
    /// This class stored information related to traffic information.
    /// </summary>
    public class TrafficIncident
    {
        #region Private Members
        /// <summary>
        /// The nature of the alert. Possible values are incident and construction
        /// </summary>
        private string type;
        /// <summary>
        ///  A more detailed description of the alert
        /// </summary>
        private string description;
        /// <summary>
        /// The URL for an image corresponding to the location of the alert
        /// </summary>
        private string imageUrl;
        /// <summary>
        /// Incident Details
        /// </summary>
        private IncidentDetails incidentDetails;
        #endregion

        #region Public Properties
        /// <summary>
        /// The nature of the alert. Possible values are incident and construction
        /// </summary>
        public string Type
        {
            get { return this.type; }
            set { this.type = value; }
        }
        /// <summary>
        ///  A more detailed description of the alert
        /// </summary>
        public string Description
        {
            get { return this.description; }
            set { this.description = value; }
        }
        /// <summary>
        /// The URL for an image corresponding to the location of the alert
        /// </summary>
        public string ImageUrl
        {
            get { return this.imageUrl; }
            set { this.imageUrl = value; }
        }
        /// <summary>
        /// Incident Details
        /// </summary>
        public IncidentDetails IncidentDetails
        {
            get { return this.incidentDetails; }
            set { this.incidentDetails = value; }
        }
        #endregion
    }

    /// <summary>
    /// This class represents the incident details
    /// </summary>
    public class IncidentDetails
    {
        #region Private Members
        /// <summary>
        /// Location where the incident took place
        /// </summary>
        private Location location;
        /// <summary>
        /// The direction of traffic flow affected by the alert
        /// </summary>
        private string direction;
        /// <summary>
        /// The severity of the alert, from 1 (least severe) to 5 (most severe)
        /// </summary>
        private int severity;
        /// <summary>
        /// The date and time this incident was reported
        /// </summary>
        private DateTime reportDate;
        /// <summary>
        /// The date and time this incident was last updated
        /// </summary>
        private DateTime updateDate;
        /// <summary>
        ///  The date and time this incident is scheduled to end
        /// </summary>
        private DateTime endDate;
        #endregion

        #region Public Properties
        /// <summary>
        /// Location where the incident took place
        /// </summary>
        public Location Location
        {
            get { return this.location; }
            set { this.location = value; }
        }
        /// <summary>
        /// The direction of traffic flow affected by the alert
        /// </summary>
        public string Direction
        {
            get { return this.direction; }
            set { this.direction = value; }
        }
        /// <summary>
        /// The severity of the alert, from 1 (least severe) to 5 (most severe)
        /// </summary>
        public int Severity
        {
            get { return this.severity; }
            set { this.severity = value; }
        }
        /// <summary>
        /// The date and time this incident was reported
        /// </summary>
        public DateTime ReportDate
        {
            get { return this.reportDate; }
            set { this.reportDate = value; }
        }
        /// <summary>
        /// The date and time this incident was last updated
        /// </summary>
        public DateTime UpdateDate
        {
            get { return this.updateDate; }
            set { this.updateDate = value; }
        }
        /// <summary>
        ///  The date and time this incident is scheduled to end
        /// </summary>
        public DateTime EndDate
        {
            get { return this.endDate; }
            set { this.endDate = value; }
        }
        #endregion
    }
}